iT邦幫忙

2024 iThome 鐵人賽

DAY 28
0
JavaScript

Vue.js學習中的細節陷阱:30天自我學習指南系列 第 28

Day 28: Vue 的元件更新優化-重新渲染問題 (Re-render)

  • 分享至 

  • xImage
  •  

今天要來談談 Vue官網效能優化 的部分,昨天我們複習了使用 JavaScript 動態載入 的特性來進行程式碼分割,這種方法將程式碼拆分成不同檔案,減少了初次渲染時請求的 JS 檔案過於肥大,進而提升加載速度,這是一種常見的優化技巧。不過,在理解了元件間的資料傳遞行為後,我們可能會想知道,Vue 的 Virtual DOM 更新機制是否可以精確地更新有資料變更的節點,而不像 React 常提到的重新渲染問題(re-render)? 今天花點時間來探討這個議題吧~

今日學習目標

  1. Props Stable - 什麼是維持Props傳遞資料的穩定度
  2. Vue 重新渲染(re-render)問題 - v-memo 使用
  3. stable computed - 保持計算屬性的穩定度

Props Stable - 維持Props傳遞資料的穩定度

a child component only updates when at least one of its received props has changed.

字面上看到「保持 props 的穩定度」一開始會不太容易直觀理解,意思是讓傳入的 props 儘量維持穩定、不頻繁變化,以避免元件內部的額外計算和不必要的重新渲染,因為一個元件可能會接收多個 props,只要其中一個有變動,子元件就會重新渲染

實際看一下範例會更好懂:

我們會發現應該只針對 activeId等於子元件id時,前面加入>符號重新渲染,這邊我們用生命週期 onUpdated 來捕捉實際元件的更新,但實際上按下按鈕是整個列表的元件都重新渲染了。

https://ithelp.ithome.com.tw/upload/images/20241011/20145251EfJThnkCgo.png

// 可以改成 isActive 布林傳入就不會導致重新渲染
<template>
  <ul>
    <ListItem
      v-for="item in messages"
      :key="item.id"
      :id="item.id"
      :message="item.message"
      :activeId="activeId" // 移除activeId
      :isActive= activeId === id // 這裡修改
    />
  </ul>
  <button @click="next">Next</button>
</template>

如果有兩個或多個 props 需要在子組件內進行比較或計算,建議在父層使用 computed 計算好結果,或是直接在樣板上以JS表達式計算完再傳入。而不是讓子元件接收props處理。這樣可以減少子元件的複雜度和依賴,使得元更具穩定性,不用因為其中依賴一個props變動導致整個元件重新渲染。

  • ListItem 接收兩個props-activeId和id 進行比較,容易因為其中一個變動而重渲染
<template>
  <div>
    <span v-show="activeId === id">&gt;</span>
    <span> {{ message }}</span>
  </div>
</template>
// 可以改為
<template>
  <div>
    <span v-show="isActive">&gt;</span>
    <span> {{ message }}</span>
  </div>
</template>

Vue 重新渲染(re-render)問題

如果有接觸過 React 多少會有耳聞重新渲染(re-render)問題,通常指的是父元件往子元件傳遞多個props資料時,但某些子元件本身接收到的props沒有變動也會重新渲染,那麼 Vue 本身是否也有這個問題?,在此之前先來看一下 Virtual DOM 的更新結構:

因為 Virtual DOM 本身是一個樹狀結構圖,當有一個中間的父節點接收到的資料更新時,會從該節點往下一起更新,是一種 DOM 模板樹結構上的依賴。即使子元件的props依賴項並未改變,父元件必須重新渲染,而影響到內嵌的子元件。

(圖片出處)

在早期2016年Vue論壇中有人談論此問題,而尤雨溪本人也有做出回應:
https://ithelp.ithome.com.tw/upload/images/20241011/20145251fqZ7GDZmTH.png

大意是: 因為早期渲染函式會在父元件的執行環境中直接執行,子元件本身沒有自己的實例狀態。由於沒有實例與函數式元件相關聯,所以很難針對先前的渲染結果進行記憶(memoization)來達到優化效果,不容易實現控制子元件在某些特定條件下不重新渲染。


v-memo

先前文章,Day 2: Vue SFC樣板(Template)和渲染函式(Render Function),有提到每個Vue3元件檔其實最終都會變成一段渲染函式,渲染函式透過 <script setup> 利用函式閉包特性將資料整個封閉起來,每個元件也都有自己資料實例狀態和一些獨立生命週期,慢慢突破在元件間記憶(memoization)上的實現

後來Vue3.2推出改版後,出一個新的指令v-memo,讓開發者自行決定元件依賴的那些props改變時,需要重新渲染來達到優化效果,主要是為了避免大型表v-for渲染時,因為大型Virtual DOM樹狀結構的重繪效能的損失

不過這種情況來說一般比較屬於特例,雖然可以避免上面Props Stable的情況發生,一般子元件DOM結構單位很小,耗損記憶體和重新執行的效率可能不成正比,就避免過度濫用

v-memo範例

  • 官方範例
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
  <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
  <p>...more child nodes</p>
</div>

props stable案例-觀察瀏覽器畫面渲染

其實一個元件接收到任何外部props資料有異動時,雖然都會立刻觸發render function導致產生新的Virtual DOM來進行比較(重新渲染),不過綁定到樣板上的資料其實後續還會進行比較,真的映射到樣板上資料有更新時,才會調動Vue runtime-dom模組去對真實瀏覽器做更新動作,並不會Virtual DOM有重繪的部分一律更新到瀏覽器上。 (註: 因為 Virtual DOM 比較的成本相對較低,可以先計算出差異再執行最小範圍的更新,從而減少不必要的 DOM 操作。)


stable computed - 保持計算屬性的穩定度

computed 穩定度的環節主要是針對用Vue監聽器進行computed的監聽的問題,像下面範例因為 computed 回傳是一般型態資料型別-布林值,可以很直觀地認為當 true/fasle 有變化時,才觸發對應的 watchEffect回調

但如果computed返回是物件,因為物件是全新的引用,有可能造成不必要資料更新

const count = ref(0)
const isEven = computed(() => count.value % 2 === 0)

watchEffect(() => console.log(isEven.value)) // true

// will not trigger new logs because the computed value stays `true`
count.value = 2
count.value = 4

// 但如果computed返回是物件,因為物件是全新的引用,有可能造成不必要資料更新

const computedObj = computed(() => {
  return {
    isEven: count.value % 2 === 0
  }
})

利用 computed 接收的參數 oldValue ,當內部屬性新值與舊值相等時,確保同一個物件的引用資料保持不變。這樣一來,Vue 可以確定實際內部所依賴的資料,來判斷是否需要進一步處理,進而避免因用物件引用位置不同所造成的更新渲染操作。

const computedObj = computed((oldValue) => {
  const newValue = {
    isEven: count.value % 2 === 0
  }
  if (oldValue && oldValue.isEven === newValue.isEven) {
    return oldValue
  }
  return newValue
})

總結

Vue其實也有重新渲染問題(re-render),但今天學到了如何減少 Vue 的重新渲染:

  • 保持 props 穩定性
    在父元件處理好必要的計算和判斷後,再傳入子元件。這樣可以減少子元件內部的複雜性,也能降低重新渲染的頻率。

  • 合理使用 v-memo,不過度濫用
    對於大型列表或子元件用有非常大型DOM結構(sub tree)並且影響效能時,可以使用 v-memo 指令以控制重新渲染的條件。

  • 合理使用 computed 和 watch
    避免 computed 屬性中每次都返回新的物件或陣列。如果要使用,可以使用 參數 oldValue 來比較新舊值,並僅在必要時返回新物件,這樣能減少重新計算和重新渲染。


學習資源

  1. https://certificates.dev/blog/performance-optimization-techniques-for-vuejs-applications
  2. https://larachamp.com/optimizing-vue-performance-with-prop-stability/
  3. https://www.youtube.com/watch?v=kqfufaZf3Og
  4. https://www.oreilly.com/library/view/learning-react-native/9781491929049/ch02.html
  5. https://ithelp.ithome.com.tw/articles/10335331
  6. https://github.com/vuejs/vue/issues/4037 (2016 社群討論)
  7. https://mokkapps.de/blog/debug-why-react-re-renders-a-component (React re-render)
  8. https://stackblitz.com/edit/react-when-does-component-render-demo?file=src%2Fcomponents%2FParent.jsx (React re-render)

上一篇
Day 27: JavaScript 模組(module) 和 Vue的程式碼分割 (code spliting)
下一篇
Day 29: Vue 的不同渲染模式 - CSR、SSR和通用渲染模式
系列文
Vue.js學習中的細節陷阱:30天自我學習指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言